﻿/*
Copyright (c) 2008 William Duff

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using System.Net;
using System.Web;
using LiveUpload.Facebook.Properties;
using System.Xml;
using System.IO;
using System.ComponentModel;
using System.Globalization;
using System.Diagnostics;

namespace LiveUpload.Facebook
{
    public static class FacebookService
    {
        #region Private Fields

        /// <summary>
        /// The endpoint that all REST Facebook API calls are made to.
        /// </summary>
        private const string FACEBOOK_REST_URL = "http://api.facebook.com/restserver.php";
        private const string FACEBOOK_VIDEO_REST_URL = "http://api-video.facebook.com/restserver.php";

        /// <summary>
        /// The version of the Facebook API in use by this application.
        /// </summary>
        private const string VERSION = "1.0";

        private const string API_KEY = "d21e471fc7c042030348260fceebaea7"; // Add your api key here.
        private const string API_SECRET = "03cfb1a14c523ad5335d5eba52296001"; // Add your api secret here.
        private const int MAX_DIMENSION = 720;
        private const int BUFFER_SIZE = 4096;
        private const int PROFILE_PICTURES_ALBUM_ID = -3; // All profile picture albums have an id of -3.

        #endregion

        #region Public Fields

        /// <summary>
        /// A unique identifier for this application that is passed as a URL parameter to access 
        /// Facebook API features.
        /// </summary>
        public static string ApiKey
        {
            get { return API_KEY; }
        }

        /// <summary>
        /// A secret identifier for this application that is passed as a URL parameter to Facebook.
        /// </summary>
        public static string ApiSecret
        {
            get { return API_SECRET; }
        }

        /// <summary>
        /// The maximum dimension for a photo as specified by Facebook.
        /// </summary>
        public static int MaxDimension
        {
            get { return MAX_DIMENSION; }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Takes over just after a photo has been uploaded, but while the upload stream is still
        /// open. Closes the upload stream and gets the response from Facebook.
        /// </summary>
        /// <param name="cookie">
        /// The UploadContext object that was used to upload a photo.
        /// </param>
        /// <returns>
        /// The response from Facebook.
        /// </returns>
        private static XmlDocument GetUploadResponse(UploadContext cookie)
        {
            HttpWebResponse response = null;

            try
            {
                cookie.RequestStream.Close();

                // Get a response.
                response = cookie.Request.GetResponse() as HttpWebResponse;
            }
            catch (WebException we)
            {
                Trace.WriteLine("Failed getting upload response with error:");
                Trace.Indent();
                Trace.WriteLine(we);
                Trace.Unindent();

                // Getting the response must have thrown an HTTP error. We can still try to get the 
                // response from the caught exception though.
                response = we.Response as HttpWebResponse;
            }

            if (response != null)
            {
                string result = null;

                // Read the response.
                Stream responseStream = response.GetResponseStream();

                using (StreamReader reader = new StreamReader(responseStream))
                {
                    StringBuilder builder = new StringBuilder();

                    try
                    {
                        while (!reader.EndOfStream)
                        {
                            builder.Append((char)reader.Read());
                        }
                    }
                    catch (System.IO.IOException ioe)
                    {
                        // Fix for work item #8786.
                        Trace.WriteLine("Failed reading Facebook upload response with error:");
                        Trace.Indent();
                        Trace.WriteLine(ioe);
                        Trace.Unindent();
                    }

                    result = builder.ToString();
                }

                responseStream.Close();

                XmlDocument responseXml = new XmlDocument();
                responseXml.LoadXml(result);
                return responseXml;
            }
            else
            {
                throw new FacebookException(Resources.FacebookResponseError);
            }
        }

        /// <summary>
        /// Generates a unique number based on the current time so that each consecutive call to 
        /// the Facebook API has a larger sequence number than the call before it.
        /// </summary>
        /// <returns>
        /// A unique sequence number that gets larger with each consecutive call.
        /// </returns>
        private static string GenerateSequenceNumber()
        {
            return FacebookParser.ConvertToUnixTimestamp(DateTime.UtcNow).ToString(CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Executes a Facebook API query with the given parameters.
        /// </summary>
        /// <param name="parameters">
        /// A sorted list of parameters.
        /// </param>
        /// <returns>
        /// The response from Facebook.
        /// </returns>
        private static XmlDocument ExecuteQuery(SortedDictionary<string, string> parameters)
        {
            // No secret specified, default to API secret.
            return ExecuteQuery(parameters, API_SECRET);
        }

        /// <summary>
        /// Executes a Facebook API query with the given parameters.
        /// </summary>
        /// <param name="parameters">
        /// A sorted list of parameters.
        /// </param>
        /// <param name="secret">
        /// A secret key to use when hashing the parameters to create a unique signature.
        /// </param>
        /// <returns>
        /// The response from Facebook.
        /// </returns>
        private static XmlDocument ExecuteQuery(SortedDictionary<string, string> parameters,
            string secretKey)
        {
            string query = GetQueryFromParameters(parameters, secretKey);
            HttpWebResponse response = null;

            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(FACEBOOK_REST_URL);
                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";

                // Write the POST parameters to Facebook.
                StreamWriter writer = new StreamWriter(request.GetRequestStream());
                writer.Write(query);
                writer.Close();

                // Get a response.
                response = request.GetResponse() as HttpWebResponse;
            }
            catch (WebException we)
            {
                Trace.WriteLine("Failed getting execute query response with error:");
                Trace.Indent();
                Trace.WriteLine(we);
                Trace.Unindent();

                // Getting the response must have thrown an HTTP error. We can still try to get the 
                // response from the caught exception though.
                response = we.Response as HttpWebResponse;

                Trace.WriteIf(response != null, "but still able to get access to the response stream.");
            }

            if (response != null)
            {
                string result = null;

                // Read the response.
                Stream responseStream = response.GetResponseStream();

                using (StreamReader reader = new StreamReader(responseStream))
                {
                    StringBuilder builder = new StringBuilder();
                    
                    try
                    {
                        while (!reader.EndOfStream)
                        {
                            builder.Append((char)reader.Read());
                        }
                    }
                    catch (System.IO.IOException ioe)
                    {
                        // Fix for work item #8786.
                        Trace.WriteLine("Failed reading Facebook execute query response with error:");
                        Trace.Indent();
                        Trace.WriteLine(ioe);
                        Trace.Unindent();
                    }

                    result = builder.ToString();
                }

                responseStream.Close();

                XmlDocument responseXml = new XmlDocument();

                try
                {
                    responseXml.LoadXml(result);
                }
                catch (XmlException e)
                {
                    // Fix for work item #10944.
                    // Write the response XML out to a file for tracing purposes.
                    Trace.WriteLine("Failed to parse Facebook API response:");
                    Trace.Indent();
                    Trace.WriteLine(result);
                    Trace.Unindent();
                    Trace.WriteLine("with error:");
                    Trace.Indent();
                    Trace.WriteLine(e);
                    Trace.Unindent();

                    throw new FacebookException(Resources.FacebookGenericError);
                }

                return responseXml;
            }
            else
            {
                throw new FacebookException(Resources.FacebookResponseError);
            }
        }

        /// <summary>
        /// Gets a string of parameters in the form of key=value, delimited by ampersands.
        /// </summary>
        /// <param name="parameters">
        /// A sorted list of parameters.
        /// </param>
        /// <param name="secret">
        /// A secret key to use when hashing the parameters to create a unique signature.
        /// </param>
        /// <returns>
        /// A string of parameters in the form of key=value, delimited by ampersands.
        /// </returns>
        private static string GetQueryFromParameters(SortedDictionary<string, string> parameters,
            string secretKey)
        {
            parameters.Add("api_key", API_KEY);
            parameters.Add("v", VERSION);
            parameters.Add("sig", GetSignature(parameters, secretKey));

            StringBuilder query = new StringBuilder();
            foreach (KeyValuePair<string, string> parameter in parameters)
            {
                query.Append(HttpUtility.UrlEncode(parameter.Key));
                query.Append('=');
                query.Append(HttpUtility.UrlEncode(parameter.Value));
                query.Append('&');
            }
            query.Remove(query.Length - 1, 1);

            return query.ToString();
        }

        /// <summary>
        /// Gets a hash of the given parameters and secret key to create a unique signature.
        /// </summary>
        /// <param name="parameters">
        /// A sorted list of parameters.
        /// </param>
        /// <param name="secret">
        /// A secret key to use when hashing the parameters to create a unique signature.
        /// </param>
        /// <returns>
        /// A hash of the given parameters and secret key.
        /// </returns>
        private static string GetSignature(SortedDictionary<string, string> parameters,
            string secretKey)
        {
            StringBuilder request = new StringBuilder();
            foreach (KeyValuePair<string, string> parameter in parameters)
            {
                request.Append(parameter.Key);
                request.Append('=');
                request.Append(parameter.Value);
            }
            request.Append(secretKey);

            MD5 md5 = MD5CryptoServiceProvider.Create();
            byte[] requestHash = md5.ComputeHash(Encoding.UTF8.GetBytes(request.ToString()));

            // Format each byte of the hash in hexadecimal.
            StringBuilder signature = new StringBuilder();
            for (int i = 0; i < requestHash.Length; i++)
            {
                signature.Append(requestHash[i].ToString("x2", CultureInfo.InvariantCulture));
            }

            return signature.ToString();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates an authentication token to be passed in as a parameter to login.php and then to 
        /// auth.getSession after the user has logged in.
        /// </summary>
        /// <returns>
        /// An authentication token.
        /// </returns>
        public static string CreateAuthToken()
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.auth.createToken");

            XmlDocument responseXml = ExecuteQuery(parameters);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            XmlElement tokenElement = responseXml.DocumentElement;

            return FacebookParser.DeserializeAuthToken(tokenElement);
        }

        /// <summary>
        /// Returns the URL to login.php to send a user to after creating their authentication 
        /// token.
        /// </summary>
        /// <param name="authToken">
        /// An authentication token.
        /// </param>
        /// <returns>
        /// A URL to login.php to send a user to.
        /// </returns>
        public static string GetAuthenticationUrl(string authToken)
        {
            string url = "https://www.facebook.com/login.php?api_key={0}&v={1}&auth_token={2}&popup=1&req_perms=publish_stream";
            return string.Format(url, API_KEY, VERSION, authToken);
        }

        /// <summary>
        /// Gets the session key bound to an authentication token, as returned by 
        /// auth.createToken. Should be called immediately after the user has logged in to 
        /// login.php.
        /// </summary>
        /// <param name="authToken">
        /// An authentication token.
        /// </param>
        /// <returns>
        /// An AccountSession associated with the user that logged in to login.php.
        /// </returns>
        public static AccountSession GetAuthSession(string authToken)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.auth.getSession");
            parameters.Add("auth_token", authToken);

            XmlDocument responseXml = ExecuteQuery(parameters);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            XmlElement sessionElement = responseXml.DocumentElement;

            return FacebookParser.DeserializeAccountSession(sessionElement);
        }

        /// <summary>
        /// Returns the URL to authorize.php to send a user to if they choose to allow extended 
        /// photo upload permissions to this application.
        /// </summary>
        /// <returns>
        /// A URL to send a user to authorize extended photo upload.
        /// </returns>
        public static string GetPhotoUploadPermissionUrl()
        {
            string url = "http://www.facebook.com/authorize.php?api_key={0}&v={1}&ext_perm=publish_stream";
            return string.Format(url, API_KEY, VERSION);
        }

        /// <summary>
        /// Returns the URL to authorize.php to send a user to if they choose to allow extended 
        /// video upload permissions to this application.
        /// </summary>
        /// <returns>
        /// A URL to send a user to authorize extended video upload.
        /// </returns>
        public static string GetVideoUploadPermissionUrl()
        {
            string url = "http://www.facebook.com/authorize.php?api_key={0}&v={1}&ext_perm=publish_stream";
            return string.Format(url, API_KEY, VERSION);
        }

        /// <summary>
        /// Returns the URL to authorize.php to send a user to if they choose to allow offline 
        /// access to this application.
        /// </summary>
        /// <returns>
        /// A URL to send a user to authorize offline access.
        /// </returns>
        public static string GetOfflineAccessPermissionUrl()
        {
            string url = "http://www.facebook.com/authorize.php?api_key={0}&v={1}&ext_perm=offline_access";
            return string.Format(url, API_KEY, VERSION);
        }

        /// <summary>
        /// Gets information about a user, including their name, a URL to a square thumbnail of 
        /// their profile picture and a list of their albums.
        /// </summary>
        /// <param name="session">
        /// A valid session.
        /// </param>
        /// <returns>
        /// An account complete with all the information required by this application.
        /// </returns>
        public static Account GetUserInfo(AccountSession session)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.fql.query");
            parameters.Add("session_key", session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());
            parameters.Add("query", "SELECT first_name, last_name, pic_square, affiliations FROM user WHERE uid=" + 
                session.OwnerId);

            XmlDocument responseXml = ExecuteQuery(parameters, session.Secret);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            Account user = new Account(session.OwnerId, session);

            XmlNodeList firstNameNodes = responseXml.DocumentElement.GetElementsByTagName("first_name");
            string firstName = firstNameNodes.Count > 0 ? firstNameNodes[0].InnerText : null;

            XmlNodeList lastNameNodes = responseXml.DocumentElement.GetElementsByTagName("last_name");
            string lastName = lastNameNodes.Count > 0 ? lastNameNodes[0].InnerText : null;

            string fullName = Resources.NoNameUser;
            
            if (!String.IsNullOrEmpty(firstName) && !String.IsNullOrEmpty(lastName))
            {
                fullName = firstName + " " + lastName;
            }
            else if (!String.IsNullOrEmpty(firstName))
            {
                fullName = firstName;
            }
            else if (!String.IsNullOrEmpty(lastName))
            {
                fullName = lastName;
            }

            user.Name = fullName;

            XmlNodeList squarePictureNodes = responseXml.DocumentElement.GetElementsByTagName("pic_square");
            user.SquarePictureUrl = squarePictureNodes.Count > 0 ? squarePictureNodes[0].InnerText : null;

            user.Albums = FacebookService.GetAlbums(user);
            user.FriendList = FacebookService.GetFriends(user);
            user.Permissions = FacebookService.GetPermissions(user);
            user.VideoUploadLimits = FacebookService.GetVideoUploadLimits(user);

            // Grab the first network the user is associated with.
            XmlNodeList affiliationNodes = responseXml.DocumentElement.GetElementsByTagName("affiliation");
            if (affiliationNodes.Count > 0)
            {
                foreach (XmlNode affiliationNode in affiliationNodes[0].ChildNodes)
                {
                    if (affiliationNode.Name == "name")
                    {
                        user.Network = affiliationNode.InnerText;
                    }
                }
            }

            return user;
        }

        /// <summary>
        /// Gets a list of albums, including cover photos, owned by the given account.
        /// </summary>
        /// <param name="user">
        /// An account.
        /// </param>
        /// <returns>
        /// A list of albums, including cover photos, owned by the given account.
        /// </returns>
        public static List<Album> GetAlbums(Account user)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.photos.getAlbums");
            parameters.Add("session_key", user.Session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());
            parameters.Add("uid", user.Id);

            XmlDocument responseXml = ExecuteQuery(parameters, user.Session.Secret);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            List<Album> albums = new List<Album>();

            XmlNodeList albumNodes = responseXml.DocumentElement.GetElementsByTagName("album");
            foreach (XmlElement albumElement in albumNodes)
            {
                Album album = FacebookParser.DeserializeAlbum(albumElement);

                // Ignore the "Profile Pictures" album.
                if (!IsProfilePicturesAlbum(user, album))
                {
                    albums.Add(album);
                }
            }

            List<Album> albumsWithCovers = albums.FindAll(delegate(Album matchingAlbum)
            {
                return (!string.IsNullOrEmpty(matchingAlbum.CoverPhotoId) && (matchingAlbum.CoverPhotoId != "0"));
            });

            if (albumsWithCovers.Count > 0)
            {
                List<Photo> coverPhotos = GetCoverPhotos(user, albumsWithCovers);
                foreach (Photo coverPhoto in coverPhotos)
                {
                    Album album = albums.Find(delegate(Album matchingAlbum)
                    {
                        return matchingAlbum.CoverPhotoId == coverPhoto.Id;
                    });
                    album.CoverPhoto = coverPhoto;
                }
            }

            return albums;
        }

        private static bool IsProfilePicturesAlbum(Account user, Album album)
        {
            // For newer Facebook accounts, the album id is a string.
            if (album.Id.Contains("_"))
            {
                return album.Id.EndsWith("_" + PROFILE_PICTURES_ALBUM_ID);
            }

            // For older Facebook accounts, the album id is a ulong.
            ulong userId = 0L; 
            ulong profilePicturesAlbumId = 0L;

            if (ulong.TryParse(user.Id, out userId))
            {
                // See http://wiki.developers.facebook.com/index.php/Profile_archive_album.
                profilePicturesAlbumId = (userId << 32) + (PROFILE_PICTURES_ALBUM_ID & 0xffffffff);

                ulong albumId;
                if (ulong.TryParse(album.Id, out albumId))
                {
                    return albumId == profilePicturesAlbumId;
                }
            }

            return false;
        }

        /// <summary>
        /// Returns a list of cover photos associated with the given list of albums.
        /// </summary>
        /// <param name="user">
        /// An account.
        /// </param>
        /// <param name="albums">
        /// A list of albums owned by the given account.
        /// </param>
        /// <returns>
        /// A list of photos that correspond to the cover of each album. Not necessarily in the
        /// same order as the list of albums.
        /// </returns>
        public static List<Photo> GetCoverPhotos(Account user, List<Album> albums)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.photos.get");
            parameters.Add("session_key", user.Session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());

            string[] coverPhotoIds = new string[albums.Count];
            for(int i = 0; i < albums.Count; i++)
            {
                coverPhotoIds[i] = albums[i].CoverPhotoId;
            }

            parameters.Add("pids", string.Join(",", coverPhotoIds));

            XmlDocument responseXml = ExecuteQuery(parameters, user.Session.Secret);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            List<Photo> coverPhotos = new List<Photo>();

            XmlNodeList photoNodes = responseXml.DocumentElement.GetElementsByTagName("photo");
            foreach (XmlElement photoElement in photoNodes)
            {
                coverPhotos.Add(FacebookParser.DeserializePhoto(photoElement));
            }

            return coverPhotos;
        }

        /// <summary>
        /// Creates an album with the given account.
        /// </summary>
        /// <param name="user">
        /// An account.
        /// </param>
        /// <param name="album">
        /// An album that has not been created yet.
        /// </param>
        /// <returns>
        /// A newly created album.
        /// </returns>
        public static Album CreateAlbum(Account user, Album album)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.photos.createAlbum");
            parameters.Add("session_key", user.Session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());
            parameters.Add("name", album.Name);

            // Optional parameters.
            if (album.Location != string.Empty)
            {
                parameters.Add("location", album.Location);
            }
            if (album.Description != string.Empty)
            {
                parameters.Add("description", album.Description);
            }
            if (album.Visible != string.Empty)
            {
                parameters.Add("visible", album.Visible);
            }

            XmlDocument responseXml = ExecuteQuery(parameters, user.Session.Secret);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            XmlElement albumElement = responseXml.DocumentElement;

            return FacebookParser.DeserializeAlbum(albumElement);
        }

        /// <summary>
        /// Prepares to upload a media object by writing out the entire HTTP POST request.
        /// </summary>
        /// <param name="user">
        /// An account.
        /// </param>
        /// <param name="albumId">
        /// An id of an album to upload photos to.
        /// </param>
        /// <param name="stream">
        /// A stream of the media object to be uploaded.
        /// </param>
        /// <param name="item">
        /// The item associated with the media object to be uploaded.
        /// </param>
        /// <returns>
        /// An UploadContext object ready to upload the media object to Facebook.
        /// </returns>
        public static UploadContext PrepareUpload(Account user, string albumId, Stream stream, 
            Item item)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            if (item.PerceivedType == "image")
            {
                parameters.Add("method", "facebook.photos.upload");
                
                // Optional parameters.
                if (albumId != string.Empty)
                {
                    parameters.Add("aid", albumId);
                }
                if (item.Title != string.Empty)
                {
                    parameters.Add("caption", item.Title);
                }
            }
            else
            {
                parameters.Add("method", "facebook.video.upload");
                parameters.Add("title", item.Title);

                // Optional parameters.
                if (!string.IsNullOrEmpty(item.Description))
                {
                    parameters.Add("description", item.Description);
                }
            }
            parameters.Add("api_key", API_KEY);
            parameters.Add("session_key", user.Session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());
            parameters.Add("v", VERSION);
            parameters.Add("sig", GetSignature(parameters, user.Session.Secret));

            string mimeBoundary = Guid.NewGuid().ToString();

            // This object holds all of the data involved in the upload process.
            UploadContext cookie;
            if (item.PerceivedType == "image")
            {
                cookie = new UploadContext(FACEBOOK_REST_URL);
            }
            else
            {
                cookie = new UploadContext(FACEBOOK_VIDEO_REST_URL);
            }
            

            // Build the HTTP POST request by hand.
            StringBuilder postHeader = new StringBuilder();
            foreach (KeyValuePair<string, string> parameter in parameters)
            {
                postHeader.AppendFormat("--{0}\r\n", mimeBoundary);
                postHeader.AppendFormat("Content-Disposition: form-data; name=\"{0}\"\r\n", 
                    parameter.Key);
                postHeader.AppendFormat("\r\n{0}\r\n", parameter.Value);
            }

            postHeader.AppendFormat("--{0}\r\n", mimeBoundary);
            postHeader.AppendFormat("Content-Disposition: form-data; filename=\"{0}\"\r\n", 
                item.OriginalFileName);
            if (item.PerceivedType == "image")
            {
                postHeader.AppendLine("Content-Type: image/jpg\r\n");
            }
            else
            {
                postHeader.AppendLine("Content-Type: video/avi\r\n");
            }

            // Store the three pieces of our HTTP POST request.
            cookie.Header = Encoding.UTF8.GetBytes(postHeader.ToString());
            cookie.DataStream = stream;
            cookie.Footer =
                Encoding.UTF8.GetBytes(string.Format("\r\n--{0}--\r\n", mimeBoundary));

            cookie.Request.ContentType =
                string.Format("multipart/form-data; boundary=\"{0}\"", mimeBoundary);
            cookie.Request.Headers.Add("MIME-version", "1.0");
            cookie.Request.ContentLength = cookie.Header.LongLength + cookie.DataStream.Length +
                cookie.Footer.LongLength;
            cookie.Request.KeepAlive = false;

            // Without this, the request will throw a OutOfMemory exception for large files.
            cookie.Request.AllowWriteStreamBuffering = false;

            // Start the stream we'll write our POST request to.
            cookie.RequestStream = cookie.Request.GetRequestStream();
            cookie.ChunkSize = Math.Max((int)(cookie.DataStream.Length / 100), 65536);

            return cookie;
        }

        /// <summary>
        /// Takes over just after a photo has been uploaded, but while the upload stream is still
        /// open. Closes the upload stream and gets the response from Facebook.
        /// </summary>
        /// <param name="cookie">
        /// The UploadContext object that was used to upload a photo.
        /// </param>
        /// <returns>
        /// A newly uploaded photo.
        /// </returns>
        public static Photo GetUploadedPhoto(UploadContext cookie)
        {
            XmlDocument responseXml = GetUploadResponse(cookie);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            XmlElement photoElement = responseXml.DocumentElement;

            return FacebookParser.DeserializePhoto(photoElement);
        }

        /// <summary>
        /// Add a list of tags to an uploaded photo.
        /// </summary>
        /// <param name="user">
        /// An account.
        /// </param>
        /// <param name="tagList">
        /// A list of tags to add to an uploaded photo that is owned by given account.
        /// </param>
        public static void AddTags(Account user, List<PhotoTag> tagList)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.photos.addTag");
            parameters.Add("session_key", user.Session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());

            bool first = true;
            StringBuilder tagsParamBuilder = new StringBuilder();
            tagsParamBuilder.Append("[");
            foreach (PhotoTag tag in tagList)
            {
                if (!first)
                {
                    tagsParamBuilder.Append(",");
                }
                else
                {
                    parameters.Add("pid", tag.PhotoId);
                    first = false;
                }
                if (!string.IsNullOrEmpty(tag.SubjectId))
                {
                    tagsParamBuilder.AppendFormat(CultureInfo.InvariantCulture, 
                        "{{\"x\":\"{0}\",\"y\":\"{1}\",\"tag_uid\":\"{2}\"}}", tag.X, tag.Y, tag.SubjectId);
                }
                else
                {
                    tagsParamBuilder.AppendFormat(CultureInfo.InvariantCulture, 
                        "{{\"x\":\"{0}\",\"y\":\"{1}\",\"tag_text\":\"{2}\"}}", tag.X, tag.Y, tag.Text);
                }
            }
            tagsParamBuilder.Append("]");
            parameters.Add("tags", tagsParamBuilder.ToString());

            XmlDocument responseXml = ExecuteQuery(parameters, user.Session.Secret);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }
        }

        /// <summary>
        /// Gets a list of friends and their UIDs.
        /// </summary>
        /// <param name="user">
        /// An account.
        /// </param>
        /// <returns>
        /// A list of friends
        /// </returns>
        public static List<Friend> GetFriends(Account user)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.fql.query");
            parameters.Add("session_key", user.Session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());
            parameters.Add("query", "SELECT first_name, last_name, uid, pic_square FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1=" +
                user.Session.OwnerId + ")");

            XmlDocument responseXml = ExecuteQuery(parameters, user.Session.Secret);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            List<Friend> friendList = new List<Friend>();
            XmlNodeList friendNodes = responseXml.DocumentElement.GetElementsByTagName("user");
            foreach (XmlElement friendElement in friendNodes)
            {
                friendList.Add(FacebookParser.DeserializeFriend(friendElement));
            }

            // You are your own friend too!
            friendList.Add(new Friend(user.Name, user.Id, user.SquarePictureUrl));

            // Sort alphabetically.
            friendList.Sort();

            return friendList;
        }

        /// <summary>
        /// Takes over just after a video has been uploaded, but while the upload stream is still
        /// open. Closes the upload stream and gets the response from Facebook.
        /// </summary>
        /// <param name="cookie">
        /// The UploadContext object that was used to upload a video.
        /// </param>
        /// <returns>
        /// A newly uploaded video.
        /// </returns>
        public static Video GetUploadedVideo(UploadContext cookie)
        {
            XmlDocument responseXml = GetUploadResponse(cookie);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            XmlElement videoElement = responseXml.DocumentElement;

            return FacebookParser.DeserializeVideo(videoElement);
        }

        /// <summary>
        /// Gets a set of Facebook permissions for the given user.
        /// </summary>
        private static Permissions GetPermissions(Account user)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.fql.query");
            parameters.Add("session_key", user.Session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());
            parameters.Add("query", "SELECT offline_access, video_upload, photo_upload FROM permissions WHERE uid=" + user.Session.OwnerId);

            XmlDocument responseXml = ExecuteQuery(parameters, user.Session.Secret);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            Permissions permissions = null;
            XmlNodeList permissionsNodes = responseXml.DocumentElement.GetElementsByTagName("permissions");
            foreach (XmlElement permissionsElement in permissionsNodes)
            {
                permissions = FacebookParser.DeserializePermissions(permissionsElement);
            }

            return permissions;
        }

        /// <summary>
        /// Gets a list of friends and their UIDs.
        /// </summary>
        /// <param name="user">
        /// An account.
        /// </param>
        /// <returns>
        /// A list of friends
        /// </returns>
        public static VideoUploadLimits GetVideoUploadLimits(Account user)
        {
            SortedDictionary<string, string> parameters = new SortedDictionary<string, string>();
            parameters.Add("method", "facebook.video.getUploadLimits");
            parameters.Add("session_key", user.Session.Key);
            parameters.Add("call_id", GenerateSequenceNumber());

            XmlDocument responseXml = ExecuteQuery(parameters, user.Session.Secret);

            if (!FacebookParser.isResponseValid(responseXml))
            {
                throw new FacebookException(FacebookParser.GetResponseError(responseXml));
            }

            XmlNodeList lengthNodes = responseXml.DocumentElement.GetElementsByTagName("length");
            string lengthText = lengthNodes.Count > 0 ? lengthNodes[0].InnerText : "0";
            double length = 0d;
            double.TryParse(lengthText, NumberStyles.Integer, CultureInfo.InvariantCulture, out length);

            XmlNodeList sizeNodes = responseXml.DocumentElement.GetElementsByTagName("size");
            string sizeText = sizeNodes.Count > 0 ? sizeNodes[0].InnerText : "0";
            double size = 0d;
            double.TryParse(sizeText, NumberStyles.Integer, CultureInfo.InvariantCulture, out size);

            return new VideoUploadLimits(length, size);
        }

        #endregion
    }
}
